package com.leedsoft.common.activiti;

import org.activiti.engine.impl.bpmn.parser.BpmnParse;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.pvm.PvmTransition;
import org.activiti.engine.impl.pvm.process.*;

import java.io.InputStream;
import java.util.*;
import java.util.List;

/**
 * Class to generate an image based the diagram interchange information in a
 * BPMN 2.0 process.
 */
public class ProcessDiagramGenerator {
    protected static final Map<String, ActivityDrawInstruction> activityDrawInstructions = new HashMap<>();

    // The instructions on how to draw a certain construct is
    // created statically and stored in a map for performance.
    static {
        // start event
        activityDrawInstructions.put("startEvent", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawNoneStartEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // start timer event
        activityDrawInstructions.put("startTimerEvent", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawTimerStartEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // signal catch
        activityDrawInstructions.put("intermediateSignalCatch", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawCatchingSignalEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // signal throw
        activityDrawInstructions.put("intermediateSignalThrow", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawThrowingSignalEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // end event
        activityDrawInstructions.put("endEvent", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawNoneEndEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // error end event
        activityDrawInstructions.put("errorEndEvent", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawErrorEndEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // error start event
        activityDrawInstructions.put("errorStartEvent", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawErrorStartEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // task
        activityDrawInstructions.put("task", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // user task
        activityDrawInstructions.put("userTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawUserTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // script task
        activityDrawInstructions.put("scriptTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawScriptTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // service task
        activityDrawInstructions.put("serviceTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawServiceTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // receive task
        activityDrawInstructions.put("receiveTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawReceiveTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // send task
        activityDrawInstructions.put("sendTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawSendTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // manual task
        activityDrawInstructions.put("manualTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawManualTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // businessRuleTask task
        activityDrawInstructions.put("businessRuleTask", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawBusinessRuleTask((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // exclusive gateway
        activityDrawInstructions.put("exclusiveGateway", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawExclusiveGateway(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // inclusive gateway
        activityDrawInstructions.put("inclusiveGateway", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawInclusiveGateway(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // parallel gateway
        activityDrawInstructions.put("parallelGateway", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawParallelGateway(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

        // Boundary timer
        activityDrawInstructions.put("boundaryTimer", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawCatchingTimerEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // Boundary catch error
        activityDrawInstructions.put("boundaryError", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawCatchingErroEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // Boundary signal event
        activityDrawInstructions.put("boundarySignal", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawCatchingSignalEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // timer catch event
        activityDrawInstructions.put("intermediateTimer", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawCatchingTimerEvent(activityImpl.getX(), activityImpl.getY(), activityImpl.getWidth(),
                        activityImpl.getHeight());
            }
        });

        // subprocess
        activityDrawInstructions.put("subProcess", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                Boolean isExpanded = (Boolean) activityImpl.getProperty(BpmnParse.PROPERTYNAME_ISEXPANDED);
                Boolean isTriggeredByEvent = (Boolean) activityImpl.getProperty("triggeredByEvent");
                if (isTriggeredByEvent == null) {
                    isTriggeredByEvent = Boolean.TRUE;
                }
                if (isExpanded != null && isExpanded == false) {
                    processDiagramCreator.drawCollapsedSubProcess((String) activityImpl.getProperty("name"), activityImpl.getX(),
                            activityImpl.getY(), activityImpl.getWidth(), activityImpl.getHeight(), isTriggeredByEvent);
                } else {
                    processDiagramCreator.drawExpandedSubProcess((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                            activityImpl.getWidth(), activityImpl.getHeight(), isTriggeredByEvent);
                }
            }
        });

        // call activity
        activityDrawInstructions.put("callActivity", new ActivityDrawInstruction() {

            public void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl) {
                processDiagramCreator.drawCollapsedCallActivity((String) activityImpl.getProperty("name"), activityImpl.getX(), activityImpl.getY(),
                        activityImpl.getWidth(), activityImpl.getHeight());
            }
        });

    }

    protected static ProcessDiagramCanvas initProcessDiagramCanvas(ProcessDefinitionEntity procDef) {
        int minX = Integer.MAX_VALUE;
        int maxX = 0;
        int minY = Integer.MAX_VALUE;
        int maxY = 0;

        if (procDef.getParticipantProcess() != null) {
            ParticipantProcess pProc = procDef.getParticipantProcess();

            minX = pProc.getX();
            maxX = pProc.getX() + pProc.getWidth();
            minY = pProc.getY();
            maxY = pProc.getY() + pProc.getHeight();
        }

        for (ActivityImpl activity : procDef.getActivities()) {

            // width
            if (activity.getX() + activity.getWidth() > maxX) {
                maxX = activity.getX() + activity.getWidth();
            }
            if (activity.getX() < minX) {
                minX = activity.getX();
            }
            // height
            if (activity.getY() + activity.getHeight() > maxY) {
                maxY = activity.getY() + activity.getHeight();
            }
            if (activity.getY() < minY) {
                minY = activity.getY();
            }

            for (PvmTransition sequenceFlow : activity.getOutgoingTransitions()) {
                List<Integer> waypoints = ((TransitionImpl) sequenceFlow).getWaypoints();
                for (int i = 0; i < waypoints.size(); i += 2) {
                    // width
                    if (waypoints.get(i) > maxX) {
                        maxX = waypoints.get(i);
                    }
                    if (waypoints.get(i) < minX) {
                        minX = waypoints.get(i);
                    }
                    // height
                    if (waypoints.get(i + 1) > maxY) {
                        maxY = waypoints.get(i + 1);
                    }
                    if (waypoints.get(i + 1) < minY) {
                        minY = waypoints.get(i + 1);
                    }
                }
            }
        }

        if (procDef.getLaneSets() != null && procDef.getLaneSets().size() > 0) {
            for (LaneSet laneSet : procDef.getLaneSets()) {
                if (laneSet.getLanes() != null && laneSet.getLanes().size() > 0) {
                    for (Lane lane : laneSet.getLanes()) {
                        // width
                        if (lane.getX() + lane.getWidth() > maxX) {
                            maxX = lane.getX() + lane.getWidth();
                        }
                        if (lane.getX() < minX) {
                            minX = lane.getX();
                        }
                        // height
                        if (lane.getY() + lane.getHeight() > maxY) {
                            maxY = lane.getY() + lane.getHeight();
                        }
                        if (lane.getY() < minY) {
                            minY = lane.getY();
                        }
                    }
                }
            }
        }

        return new ProcessDiagramCanvas(maxX + 10, maxY + 10, minX, minY);
    }

    protected interface ActivityDrawInstruction {
        void draw(ProcessDiagramCanvas processDiagramCreator, ActivityImpl activityImpl);
    }

    /**
     * [生成流程图，返回输入流]
     */
    public static InputStream generateInstDiagram(ProcessDefinitionEntity procDef, List<String> hiLightActs, List<String> activeActs) {
        return generateDiagram(procDef, hiLightActs, activeActs).generateImage("png");
    }

    /**
     * [生成流程图，返回输入流]
     */
    public static InputStream getDefDiagram(ProcessDefinitionEntity procDef) {
        return generateDiagram(procDef, null, null).generateImage("png");
    }

    /**
     * [生成实例流程图，返回流程图画布]
     */
    protected static ProcessDiagramCanvas generateDiagram(ProcessDefinitionEntity procDef, List<String> hiLightActs, List<String> activeActIds) {
        ProcessDiagramCanvas canvas = initProcessDiagramCanvas(procDef);

        // Draw activities and their sequence-flows
        // 循环当前流程定义中的所有节点，绘制节点和节点上的流程线，如果节点已经执行过则高亮显示
        for (ActivityImpl activity : procDef.getActivities()) {
            drawActivityFlow(canvas, activity);
        }

        if ((hiLightActs != null && activeActIds != null)) {
            // 高亮显示：已执行的节点、已执行的顺序流、正在执行的节点
            for (ActivityImpl activity : procDef.getActivities()) {
                if (hiLightActs.contains(activity.getId())) {
                    drawHiLightNode(canvas, activity);
                    drawHiLightFlow(canvas, activity, hiLightActs, activeActIds);
                } else if (activeActIds.contains(activity.getId())) {
                    drawActiveNode(canvas, activity);
                }
            }
        }
        return canvas;
    }


    /**
     * 绘制当前节点的所有流出流程线，如果流程线已被执行过，则高亮显示
     */
    protected static void drawHiLightFlow(ProcessDiagramCanvas canvas,
                                          ActivityImpl activity, List<String> hiLightActs, List<String> activeActIds) {
        boolean isHiLightFlow;
        ActivityImpl destAct;
        for (PvmTransition sequenceFlow : activity.getOutgoingTransitions()) {
            isHiLightFlow = false;
            destAct = (ActivityImpl) sequenceFlow.getDestination();
            //当前flow的dest，依然是历史节点或者是激活节点，则当前flow高亮
            if (hiLightActs.contains(destAct.getId()) || activeActIds.contains(destAct.getId())) {
                isHiLightFlow = true;
            }
            if (!isHiLightFlow) continue;

            drawFlow(canvas, (TransitionImpl) sequenceFlow, true);
        }
    }

    private static void drawFlow(ProcessDiagramCanvas canvas, TransitionImpl seqflow, boolean isHiLight) {
        List<Integer> waypoints = seqflow.getWaypoints();
        //将顺序流的路径拐点，修正为只有起点和终点。并防止顺序流横穿节点。
        Point startPoint = new Point(waypoints.subList(0, 2));
        Point endPoint = new Point(waypoints.subList(waypoints.size() - 2, waypoints.size()));
        ActivityImpl activity = seqflow.getDestination();
        if (startPoint.x < activity.getX()) {
            endPoint.x = activity.getX();
        } else {
            endPoint.x = activity.getX() + activity.getWidth();
        }

        boolean drawIndicator = false;
        if (seqflow.getSource().getProperty("type").equals("exclusiveGateway")) {
            drawIndicator = true;
        }
        String name = (String) seqflow.getProperty("name");
        if (isHiLight)
            canvas.drawHiLightFlow(startPoint, endPoint, drawIndicator, name);
        else
            canvas.drawFlow(startPoint, endPoint, drawIndicator, name);
    }

    public static class Point {
        int x;
        int y;

        public Point(List<Integer> list) {
            this.x = list.get(0);
            this.y = list.get(1);
        }
    }

    /**
     * [绘制流程图中的节点和节点上的流程线]
     */
    protected static void drawActivityFlow(ProcessDiagramCanvas canvas, ActivityImpl activity) {
        // 获取节点类型
        String type = (String) activity.getProperty("type");
        ActivityDrawInstruction drawInstruction = activityDrawInstructions.get(type);
        if (drawInstruction != null) {

            drawInstruction.draw(canvas, activity);

            // Gather info on the multi instance marker
            boolean multiInstanceSequential = false, multiInstanceParallel = false, collapsed = false;
            String multiInstance = (String) activity.getProperty("multiInstance");
            if (multiInstance != null) {
                if ("sequential".equals(multiInstance)) {
                    multiInstanceSequential = true;
                } else {
                    multiInstanceParallel = true;
                }
            }

            // Gather info on the collapsed marker
            Boolean expanded = (Boolean) activity.getProperty(BpmnParse.PROPERTYNAME_ISEXPANDED);
            if (expanded != null) {
                collapsed = !expanded;
            }

            // Actually draw the markers
            canvas.drawActivityMarkers(activity.getX(), activity.getY(), activity.getWidth(), activity.getHeight(),
                    multiInstanceSequential, multiInstanceParallel, collapsed);
        }

        // incoming transitions of activity
        for (PvmTransition sequenceFlow : activity.getIncomingTransitions()) {
            drawFlow(canvas, (TransitionImpl) sequenceFlow, false);
        }
    }

    /**
     * [绘制高亮显示的节点]
     */
    private static void drawHiLightNode(ProcessDiagramCanvas canvas, ActivityImpl activity) {
        canvas.drawHiLightNode(activity.getX(), activity.getY(), activity.getWidth(), activity.getHeight());

    }

    /**
     * 绘制激活节点（当前正在执行）
     */
    private static void drawActiveNode(ProcessDiagramCanvas canvas, ActivityImpl activity) {
        canvas.drawHiLightNode(activity.getX(), activity.getY(),
                activity.getWidth(), activity.getHeight(), ProcessDiagramCanvas.COLOR_ACTIVE);
    }

}
